﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace JsiCommon.javaScriptInterop
{
    public static class JsiCompiler
    {
        public static void Dumm()
        {
            //var x = "ABC";
            var x = new ImageButton();
            x.ID = "AA";
            //var z = JQueryGlobals.JQuery(x).Attr("A", "B");
            Console.Write(CompileToJs(() => JQueryGlobals.JQuery(x).Attr("A", x.CssClass)));
            Console.ReadKey();
        }

        private static readonly object InitializerLock = new object();
        private static bool _initialized = false;
        private static Dictionary<MemberInfo, string> _nameMap;

        private static void InitializeCompiler()
        {
            if(_initialized)
                return;
            lock(InitializerLock)
            {
                if (_initialized)
                    return;

                _nameMap = new Dictionary<MemberInfo, string>();
                _nameMap.Add(typeof(Image).GetMember("ImageUrl")[0], "src");
                _nameMap.Add(typeof(WebControl).GetMember("CssClass")[0], "class");
                _nameMap.Add(typeof(TextBox).GetMember("Text")[0], "value");

                _initialized = true;
            }
        }

        public static void AssociateMember(MemberInfo memberInfo, string memberJsName)
        {
            _nameMap.Add(memberInfo, memberJsName);            
        }

        private static IJsiExpression CompileInt(MethodCallExpression exp)
        {
            IJsiExpression methodAccess;
            if (exp.Object == null)
            {
                var attribute = exp.Method.GetCustomAttributes(typeof(JsNameAttribute), false).FirstOrDefault() as JsNameAttribute;
                if (attribute == null)
                    throw new ArgumentException(
                        "Can not transform native method without JsNameAttribute to js expression");
                methodAccess = new JsiGlobalObject(attribute.Name);
            }
            else
            {
                var methodName = GetJsiMemberName(exp.Method);
                if (methodName == null)
                    return JsiNonevaluable.GetInstance();
                var callingObject = Compile(exp.Object);
                if (callingObject is JsiNonevaluable)
                    return JsiNonevaluable.GetInstance();
                methodAccess = new JsiMemberAccess(methodName, callingObject);
            }
            var args = (from x in exp.Arguments select Compile(x)).ToList();
            if (args.Any(x=>x is JsiNonevaluable))
                return JsiNonevaluable.GetInstance();
            return new JsiFunctionCall(methodAccess, args);
        }

        private static string GetJsiMemberName(MemberInfo info)
        {
            string mappedName;
            if(_nameMap.TryGetValue(info, out mappedName))
                return mappedName;

            var nameAttribute = info.GetCustomAttributes(typeof(JsNameAttribute), false).FirstOrDefault() as JsNameAttribute;
            if(nameAttribute != null)
                return nameAttribute.Name;
            var memberInfoName = info.Name;
            var declaringType = info.DeclaringType;
            if (!declaringType.GetCustomAttributes(typeof(JsTypeAttribute), false).Any())
                return null;
                //throw new UnknownExpressionException("Can not access method '" + info.Name + "', because it does not have JsName attribute, its type does not have JsType attr, and there is no mapping for this type name in member naming map");
            var defaultNameConversionAttribute =
                declaringType.GetCustomAttributes(typeof (JsDefaultNameConversionAttribute), false).FirstOrDefault()
                as JsDefaultNameConversionAttribute;
            if(defaultNameConversionAttribute == null || defaultNameConversionAttribute.NameConversion == JsDefaultNameConversion.None)
                return memberInfoName;
            return memberInfoName.ToLower();
        }

        private static IJsiExpression CompileInt(MemberExpression exp)
        {
            if(exp.Expression == null)
            {
                var attribute = exp.Member.GetCustomAttributes(typeof(JsNameAttribute), false).FirstOrDefault() as JsNameAttribute;
                if (attribute == null)
                    return JsiNonevaluable.GetInstance();
                    //throw new ArgumentException("Can not transform native method without JsNameAttribute to js expression");
                return new JsiGlobalObject(attribute.Name);
            }
            var expression = Compile(exp.Expression);
            if (expression is JsiNonevaluable)
                return JsiNonevaluable.GetInstance();

            var memberName = GetJsiMemberName(exp.Member);
            if (memberName == null)
                return JsiNonevaluable.GetInstance();

            return new JsiMemberAccess(memberName, expression);
        }

        private static IJsiExpression CompileInt(ConstantExpression exp)
        {
            if (exp.Type == typeof(string))
                return new JsiLiteral((string) exp.Value);
            if (typeof(Control).IsAssignableFrom(exp.Type))
                return new JsiControl((Control)exp.Value);
            return JsiNonevaluable.GetInstance();
        }

        public static IJsiExpression Compile(Expression<Action> exp)
        {
            InitializeCompiler();
            return Compile((Expression)exp);
        }

        //private static IEnumerable<Jsi>

        public static IJsiExpression Compile(Expression exp)
        {
            InitializeCompiler();
            if (exp.NodeType == ExpressionType.Lambda)
                return Compile(((LambdaExpression)exp).Body);
            //Detect if need to fall, or just evaluate expression
            if (!IsJsType(exp.Type) && !IsJsiType(exp.Type))
            {
                return JsiNonevaluable.GetInstance();
            }

            IJsiExpression retExp;
            if (exp.NodeType == ExpressionType.Call)
                retExp = CompileInt((MethodCallExpression)exp);
            else if (exp.NodeType == ExpressionType.MemberAccess)
                retExp = CompileInt((MemberExpression)exp);
            else if (exp.NodeType == ExpressionType.Constant)
                retExp = CompileInt((ConstantExpression)exp);
            else
                retExp = JsiNonevaluable.GetInstance();

            if (retExp is JsiNonevaluable && IsJsType(exp.Type))
            {
                var a = Expression.Lambda(exp).Compile().DynamicInvoke();
                if (a is string)
                    return new JsiLiteral((string) a);
                if (a is Control)
                    return new JsiControl((Control) a);
            }
            return retExp;
        }

        private static bool IsJsType(Type type)
        {
            return type == typeof(string) || type.IsPrimitive || typeof(Control).IsAssignableFrom(type);
        }

        private static bool IsJsiType(Type type)
        {
            return type.GetCustomAttributes(typeof (JsTypeAttribute), false).Length > 0;
        }

        public static string CompileToJs(Expression<Action> exp)
        {
            var expression = Compile(exp);
            var b = new StringBuilder();
            expression.Compile(b);
            return b.ToString();
        }

        public static string CompileToJs(params Expression<Action>[] expList)
        {
            InitializeCompiler();
            var expression = new JsiActionSequence(from x in expList select Compile(x));
            var b = new StringBuilder();
            expression.Compile(b);
            return b.ToString();
        }
    }
}